package com.cty.nio;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Scanner;

/**
 * @Auther: cty
 * @Date: 2020/4/17 18:06
 * @Description: NIO服务器端
 * NIO New IO 提供同步阻塞编程方式和同步非阻塞编程方式，JDK1.4开始支持
 *
 * 注意：“非阻塞”是指在等待数据阶段是轮询的，而不是阻塞的；但是在操作数据阶段的读方法和写方法都是阻塞方法。
 *
 * 通道（ServerSocketChannel or SocketChannel）：注册到 多路复用器，同时设置状态。
 * 多路复用器（Selector）：选中处于可进行IO操作状态的通道，并返回这些通道对应的 选择键 集合。
 * 选择键（SelectionKey）：包含该通道的状态信息，并可获取该通道。
 *
 * [NIO的SelectionKey(选择键)](https://www.jianshu.com/p/d33f2f6cdba0)
 * 未读 [关于 NIO 你不得不知道的一些“地雷”](https://www.jianshu.com/p/1af407c043cb)
 * 未读 [Netty 源码解析 ——— 基于 NIO 网络传输模式的 OP_ACCEPT、OP_CONNECT、OP_READ、OP_WRITE 事件处理流程](https://cloud.tencent.com/developer/article/1152647)
 * @version: 1.0
 */
public class NIOServer implements Runnable{
    // 定义多路复用器（选择器），用于注册通道
    private Selector selector;
    // 定义两块缓存，分别用于读和写（单位字节）
    private ByteBuffer readBuffer = ByteBuffer.allocate(1024);
    private ByteBuffer writeBuffer = ByteBuffer.allocate(1024);

    // 构造器
    NIOServer(int port){
        init(port);
    }

    // 主函数
    public static void main(String[] args) {
        new Thread(new NIOServer(9999)).start();
    }

    // 初始化函数
    private void init(int port){
        try {
            System.out.println("Server starting at port " + port + " ...");
            // 开启多路复用器
            this.selector = Selector.open();
            // 开启服务通道
            ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
            // 将服务通道设置为非阻塞模式
            serverSocketChannel.configureBlocking(false);
            // 给服务通道绑定端口
            serverSocketChannel.bind(new InetSocketAddress(port));
            // 将服务通道注册到多路复用器，并设置为可接收客户端连接状态
            serverSocketChannel.register(this.selector, SelectionKey.OP_ACCEPT);

        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void run() {
        while(true){
            try {
                // 注册到多路复用器，且可进行IO操作的通道将被选中
                this.selector.select();  // 【等待数据阶段】 select()-阻塞等待就绪  selectNow()-非阻塞轮询就绪
                // 返回选中通道选择键集合
                Iterator<SelectionKey> keys = this.selector.selectedKeys().iterator();
                while(keys.hasNext()){
                    SelectionKey key = keys.next();
                    // 将本次要处理的通道对应的选择键从选择键集合中删除，下次循环根据新的选择键集合再次执行必要的业务逻辑
                    keys.remove();

                    // 通道是否有效
                    if(key.isValid()){
                        // 通道是否处于可接收客户端连接状态
                        try{
                            if(key.isAcceptable()){
                                accept(key);
                            }
                        }catch (Exception e){
                            key.cancel();
                        }

                        // 通道是否处于可读状态
                        try{
                            if(key.isReadable()){
                                read(key);
                            }
                        }catch (Exception e){
                            key.cancel();
                        }

                        // 是否处于可写状态
                        try{
                            if(key.isWritable()){
                                write(key);
                            }
                        }catch (Exception e){
                            key.cancel();
                        }

                    }
                }

            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    private void accept(SelectionKey key){
        try {
            // 此通道是init方法中注册到Selector上的ServerSocketChannel
            ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
            /**
             * 方法：ServerSocketChannel . SocketChannel accept()
             * 参数：无
             * 返回：新连接的套接字通道，或null（如果通道处于非阻塞模式且当前无连接请求）
             * 作用：接受来自客户端的与该通道套接字连接的请求并建立连接
             * 说明：
             *      该方法受通道阻塞模式影响，若当前线程调用该方法时：
             *          若通道处于非阻塞模式
             *              若有来自客户端的连接请求，则建立连接，返回SocketChannel对象
             *              若无来自客户端的连接请求，返回null
             *          若通道处于阻塞模式
             *              会一直等待，直到出现连接请求或发生IO错误
             *
             *      当服务器端与客户端建立连接成功，两端就变成了对等地位，都使用SocketChannel对象进行IO操作。
             */
            SocketChannel socketChannel = serverSocketChannel.accept();
            // 返回SocketChannel默认处于阻塞模式，手动设置为非阻塞模式
            socketChannel.configureBlocking(false);
            // 将对应客户端通道注册到多路复用器，并设置为可读取数据状态
            socketChannel.register(this.selector, SelectionKey.OP_READ);

            System.out.println("Connected with " + socketChannel.getRemoteAddress() + " client.");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private void read(SelectionKey key){
        SocketChannel socketChannel = null;
        try {
            this.readBuffer.clear();  // 清空读缓存
           socketChannel = (SocketChannel)key.channel();  // 获取通道
            System.out.println("Reading from " + socketChannel.getRemoteAddress() + " client: ");
            // 将通道中的数据读取到缓存中

            int readLength = socketChannel.read(readBuffer);  // 【读数据 操作数据阶段-阻塞方法】
            if(readLength == -1){  // 若客户端未在通道中写入数据
                key.channel().close();  // 关闭通道
                key.cancel();  // 关闭连接
                return;
            }
            // 执行到这里，说明客户端在通道中写入数据
            this.readBuffer.flip();  // 重置缓存游标
            // 定义一个字节数据，从读缓存中获取数据。 Buffer.remaining() -> 获取Buffer中的有效字节数
            byte[] bytes = new byte[readBuffer.remaining()];
            readBuffer.get(bytes);
            System.out.println(new String(bytes, "UTF-8"));

            // 将该通道重新注册到多路复用器，并设置为可写出数据状态
            socketChannel.register(this.selector, SelectionKey.OP_WRITE);
        } catch (IOException e) {
//            e.printStackTrace();
            try {
                System.out.println("Connection reset with " + socketChannel.getRemoteAddress() + " client.");
                key.channel().close();  // 关闭通道
                key.cancel();  // 关闭连接
            } catch (IOException ex) {
                ex.printStackTrace();
            }
        }

    }

    private void write(SelectionKey key){
        this.writeBuffer.clear();
        SocketChannel socketChannel = (SocketChannel) key.channel();
        Scanner scanner = new Scanner(System.in);

        try {
            System.out.println("Put message for sending to "+ socketChannel.getRemoteAddress() + " client > ");
            String message = scanner.nextLine();
            writeBuffer.put(message.getBytes("UTF-8"));
            writeBuffer.flip();
            socketChannel.write(writeBuffer);  // 【写数据】

            socketChannel.register(this.selector, SelectionKey.OP_READ);
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        } catch (IOException e) {
//            e.printStackTrace();
            try {
                System.out.println("Connection reset with " + socketChannel.getRemoteAddress() + " client.");
                key.channel().close();  // 关闭通道
                key.cancel();  // 关闭连接
            } catch (IOException ex) {
                ex.printStackTrace();
            }
        }
    }
}
